/*
 * @author 渡一
 * @date 2024-12-07 17:47:07
 * @description 元素进入视口的动画指令；
 * @usage v-slide-in="{ distance: 100, duration: 500, timing: 'ease-out' }"
 * @see 🚀 https://www.douyin.com/follow
 * @status done
 */
import type { Directive, DirectiveBinding } from 'vue'

const DISTANCE = 100,
  DURATION = 500,
  TIMING = 'ease-out'
let distance: number | null, duration: number | null, timing: string | null

interface ISlideInDirectiveOptions {
  /** 动画距离 */
  distance?: number
  /** 动画时长 */
  duration?: number
  /** 动画时间曲线 */
  timing?: string
}

const map = new WeakMap()

/**
 * 创建一个IntersectionObserver对象，用于监听元素是否进入视口
 */
const ob = new IntersectionObserver((entries) => {
  // 遍历所有被监听的元素
  for (const entry of entries) {
    // 如果元素进入视口
    if (entry.isIntersecting) {
      // 获取元素对应的动画
      const animation = map.get(entry.target)
      if (animation) {
        // 如果有动画，则播放动画
        animation.play()
        // 取消观察目标元素
        ob.unobserve(entry.target)
      }
    }
  }
})

/**
 * 判断元素是否在视口下方
 * @param el 需要判断的元素
 * @returns
 */
function isBelowViewport(el: HTMLElement) {
  // 获取元素的位置信息
  const rect = el.getBoundingClientRect()
  // 判断元素顶部是否在视口下方
  return rect.top > window.innerHeight
}

/**
 * 滑入动画指令
 */
export const slideIn: Directive = {
  // 指令被绑定到元素时调用
  mounted(
    el: HTMLElement,
    binding: DirectiveBinding<ISlideInDirectiveOptions | null>
  ) {
    if (binding.value) {
      distance = binding.value.distance || DISTANCE
      duration = binding.value.duration || DURATION
      timing = binding.value.timing || TIMING
    }
    // 如果元素不在视口内，则返回
    if (!isBelowViewport(el)) {
      return
    }
    // 获取指令的值，如果没有设置，则使用默认值
    // console.log(distance, duration, timing)

    // 创建动画
    const animation = el.animate(
      [
        { transform: `translateY(${distance || DISTANCE}px)`, opacity: 0.5 },
        { transform: 'translateY(0)', opacity: 1 }
      ],
      {
        // 设置动画的持续时间，如果duration参数存在，则使用duration参数，否则使用默认的DURATION
        duration: duration || DURATION,
        // 设置动画的缓动函数，如果timing参数存在，则使用timing参数，否则使用默认的TIMING
        easing: timing || TIMING,
        // 设置动画的填充模式，这里设置为forwards，表示动画结束后保持最后一帧的状态
        fill: 'forwards'
      }
    )
    // 暂停动画
    animation.pause()
    // 将动画存储到map中
    map.set(el, animation)
    // 监听元素
    ob.observe(el)
  },
  // 指令从元素上解绑时调用
  unmounted(el: HTMLElement) {
    // 停止监听元素
    ob.unobserve(el)
  }
}
